Skip to content

feat(paper-equities): the equities paper profile — tracked config, venue selection, deployment artifacts, runbook (#370 B2) - #386

Merged
eaitbrahim merged 2 commits into
mainfrom
feat/370-equities-profile
Aug 19, 2026
Merged

feat(paper-equities): the equities paper profile — tracked config, venue selection, deployment artifacts, runbook (#370 B2)#386
eaitbrahim merged 2 commits into
mainfrom
feat/370-equities-profile

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

What

Phase B2 of #370 (PRD docs/superpowers/specs/2026-08-18-keel-broker-alpaca-prd.md, Phase B / §5, FR-9/FR-10 posture): the equities paper profile — keel's daily turtle rules on US equities via Alpaca's PAPER API, on their own database, accruing hash-chained paper evidence end-to-end — plus the minimal engine wiring it needs: config-driven venue selection.

The profile (tracked, mirrors the paper-hourly pattern)

  • config.paper-equities.yamlbroker: {name: alpaca, endpoint: paper, data_feed: iex}; market_data.granularities is ONE_DAY only (a daily clock — hourly bars exist only within sessions and daily rules do not read them; stated in comments); interval_sec: 86400; weights sum to exactly 1.000; risk_pct: 0.01; caps mirroring the paper profiles; the paper-hourly-style honesty caveat in the header.
  • Deployment artifactscom.keel.paper-equities.plist (strict-XML validated; schedule below), paper-equities-run.sh (UTC-day stamp, set -e, stamp only after a successful cycle, session-window guard), keel-equities wrapper (config + keel-equities.db travel as a pair).
  • Runbookdocs/operator-runbook.md gains "The equities paper profile": why it exists (evidence on a session-bound venue; the same honest no-edge caveat), bootstrap (Alpaca paper account, env vars, keel migrate --db keel-equities.db, rules add --kind turtle_breakout --product SYMBOL --params '{"granularity": "ONE_DAY"}' + rules promote --force with the documented bypass rationale, fetch warm — B1 makes weekends quiet), the fourth profile-comparison column, attestation semantics (operator-supplied per (alpaca, SYMBOL), AAOIFI/IFSB-class sources per the fiqh source review; the engine never classifies), rail 17 as ACATS transfer-out, the T+1 × daily-cadence interaction (immaterial for entries; exits never spend), and the two operator-verified opt-outs (stock lending OFF — qabd; high-yield sweep OFF — riba; where to verify each in the Alpaca dashboard). .env.example wires the new key names.

Venue selection (the engine half)

A new broker: config section — name (→ keel.brokers entry-point discovery), endpoint (paper|live), data_feed (iex|sip), both validated at load — threaded through the one broker-construction seam every CLI/agent/TUI path shares (keel/commands/_common.py::_build_broker), plus telemetry's bind_venue in _load_cfg. Secrets follow the CDP pattern: load_alpaca_secrets reads ALPACA_API_KEY_ID/ALPACA_API_SECRET_KEY from the environment or .env; the missing-keys error names the venue and both variables.

When the section is absent, the Coinbase construction path is byte-compatible — pinned by a test that drives the real _build_broker against monkeypatched RESTClient/CoinbaseClient and asserts the exact kwargs — and every pre-existing test stays green unchanged. The config golden baseline is regenerated deliberately (its own documented step) for the new field.

The candidate set — PAPER CANDIDATES

MSFT, AAPL, GOOGL, NVDA, COST — chosen for liquidity (deep daily books, long split-adjusted history, IEX data quality on mega-caps) and as the kind of low-debt large cap a screen could be run on, flat 20% each (flatness states no view). Classification is operator-attested per (alpaca, SYMBOL) before any live consideration; the engine never classifies and this file asserts nothing religiously. There is NO PROVEN EDGE on any asset class; the profile exists for evidence, not profit.

Schedule — a deliberate deviation, stated

The plist fires 10:00–15:00 local (ET), inside the US regular session — not ~30–60 min after the 16:00 ET close. Reason: B1's session gate (#385) skips the whole cycle whenever the venue clock answers closed, so an after-close trigger would log market_closed and never evaluate a bar — zero evidence, forever. The daily bar that closes at 16:00 ET is evaluated at the next session's open (signal on close, execute next open — the conventional daily-system semantics). The runner's window guard (local hours 10–15) keeps a pre-open/after-close closed-market skip (which exits 0) from being stamped as the day's work. DST: the host's zone is America/New_York, so fixed local triggers keep their ET meaning across both US transitions (what moves is the UTC instant, never the distance from the open); the caveat for other hosts is in the plist comment and the runbook.

Deliberately NOT here

Test-first + gates

tests/test_paper_equities_profile.py (35 tests) was written red first — config/broker parsing, alpaca construction, missing-secrets error, plist strict-parse + triggers, the runner harness (real script, date shim, stubbed keel) including the pre-open/after-close stamp regressions, wrapper, runbook pins — then drove the implementation. The byte-compat default pin passed before and after, as a pin should.

  • uv run pytest -q3225 passed, 2 skipped (both pre-existing expected skips)
  • uv run ruff check keel tests packages — clean
  • uv run mypy — clean (250 files)

Refs #370 (Phase C continues — not a fix)

…nue selection, deployment artifacts, runbook (#370 B2)

Phase B2 of #370 (PRD docs/superpowers/specs/2026-08-18-keel-broker-alpaca-prd.md,
Phase B / §5): keel's daily turtle rules on US equities via Alpaca's PAPER API, on
their own database, accruing hash-chained paper evidence end-to-end.

Deliverables:

- config.paper-equities.yaml — the tracked profile: broker alpaca/paper/iex,
  ONE_DAY only (a daily clock; hourly bars exist only within sessions and daily
  rules do not read them), interval_sec 86400, weights summing to 1.000,
  risk_pct 0.01, caps mirroring the paper profiles, and the paper-hourly-style
  honesty caveat in the header.
- Deployment artifacts mirroring the paper-hourly trio: com.keel.paper-equities
  .plist (strict-XML validated), paper-equities-run.sh (UTC-day stamp, set -e,
  stamp only after success, local-hour window guard), keel-equities wrapper
  (config+db travel as a pair).
- Runbook: "The equities paper profile" — bootstrap, the fourth profile-comparison
  column, attestation semantics (operator-supplied per (alpaca, SYMBOL), AAOIFI/
  IFSB-class sources, the engine never classifies), rail 17 as ACATS transfer-out,
  the T+1 x daily-cadence interaction (immaterial for entries; exits never spend),
  and the two operator-verified opt-outs (stock lending OFF for qabd, high-yield
  sweep OFF for riba). .env.example gains the ALPACA_* key names.

Venue selection — the minimal engine wiring the profile needs. New `broker:`
config section (name -> keel.brokers entry-point discovery; endpoint paper|live;
data_feed iex|sip, both validated at load) threaded through the one broker-
construction seam the CLI/agent/TUI paths share, keel/commands/_common.py::
_build_broker, plus telemetry's bind_venue in _load_cfg. Secrets follow the CDP
pattern: load_alpaca_secrets reads ALPACA_API_KEY_ID/ALPACA_API_SECRET_KEY from
the environment or .env, and the missing-keys error names the venue and both
vars. WHEN THE SECTION IS ABSENT the construction path is byte-compatible
Coinbase — pinned by test against monkeypatched RESTClient/CoinbaseClient, and
every pre-existing test stays green unchanged. The config golden baseline is
regenerated (deliberately, per its own docs) for the new field.

The allowlist is PAPER CANDIDATES: MSFT, AAPL, GOOGL, NVDA, COST — chosen for
liquidity and as the kind of low-debt large cap a screen COULD be run on, flat
20% each (no view). Classification is operator-attested per (alpaca, SYMBOL)
before any live consideration; the engine never classifies and the file asserts
nothing religiously. There is NO PROVEN EDGE on any asset class; the profile
exists for evidence, not profit.

Schedule note, deliberate deviation from the obvious copy-paste: the plist fires
10:00-15:00 local (ET) — INSIDE the US regular session, not ~30-60 min after the
16:00 close — because B1's session gate skips the whole cycle whenever the venue
clock answers closed, so an after-close trigger would log market_closed and never
evaluate a bar. The daily bar is evaluated at the next session's open (signal on
close, execute next open); the runner's window guard keeps a pre-open/after-close
closed-market skip (exit 0) from being stamped as the day's work. The host is
America/New_York, so fixed local triggers keep their ET meaning across both US
DST transitions.

Deliberately NOT here: keel/assets screening venue semantics stay hardcoded to
coinbase (#233 live-path work; runbook open item — equity classifications live
in the operator's records, the profile trades unattested paper candidates);
deployment to the operator's machine (needs their Alpaca paper credentials —
the runbook is the bootstrap); Phase C cost fidelity/DCA benchmark.

Refs #370
…ing docs, clock-failure retry

Eight adversarial-review findings on PR #386 (#370 B2):

1. Rail 14 keyed every BUY on the hardcoded DEFAULT_VENUE (coinbase), so an alpaca
   deployment was gated on a coinbase record nothing writes (out of the box: $0
   allowance, every BUY vetoed with advice naming the wrong venue). The rail now reads
   the deployment's venue through the SAME ContextVar binding _load_cfg makes for
   telemetry (bind_venue(config.broker.name), via current_venue()); coinbase remains
   the answer when nothing is bound. subscription attest/set default --venue to the
   bound venue and show's empty advice names it.
2. Runbook gains the PRD 5/6.4 account-posture section: cash accounts only (margin
   borrowing is riba; also sidesteps PDT's $25k margin-account threshold), PDT
   explained, T+1 cross-referenced not duplicated, enforcement fenced to #372.
3. Runbook fences dividend purification (FR-10 events, math against the attested
   ratio, recorded disposition) as the Phase B3 slice.
4. assets holdings auth advice is venue-aware: alpaca names ALPACA_API_KEY_ID/
   ALPACA_API_SECRET_KEY; coinbase keeps the CDP advice.
5. keel agent (single-cycle) exits MARKET_CLOCK_UNAVAILABLE_EXIT (5) on a
   market_clock_unavailable skip, mirroring DATA_NOT_READY_EXIT, so the runner's
   set -e declines to stamp the UTC day and the next trigger retries; market_closed
   still exits 0 and stamps (correct cadence bookkeeping). Runner comments state the
   two skip kinds.
6. DST caveat restated honestly in plist/runbook/config: the schedule is correct on
   an ET-anchored host (or within ±4h); elsewhere re-anchor triggers to 10:00-15:00
   ET; the local-hours guard is a backstop, not a drift absorber.
7. Window wording aligned everywhere: triggers at 10:00-15:00 local; the runner's
   window is 10:00 inclusive to 16:00 exclusive (the 15:00 trigger runs).
8. Typo (ABSSENCE) and 'low-debt large cap' phrasing (an unattested leverage fact)
   replaced with 'liquid large caps', disclaimer intact.
@eaitbrahim
eaitbrahim merged commit a7b2737 into main Aug 19, 2026
5 checks passed
@eaitbrahim
eaitbrahim deleted the feat/370-equities-profile branch August 19, 2026 07:51
eaitbrahim added a commit that referenced this pull request Aug 20, 2026
Minor, not patch. Three things since v0.9.3 change what an implementor or a
deployment can rely on:

* A NEW DISTRIBUTION. `keel-broker-alpaca` (#382, #384) plus the
  paper-equities profile that selects it (#386), so a deployment can now be
  US equities via the broker port rather than crypto only.
* THE PORT CONTRACT MOVED TWICE. `market_clock`/`market_schedule` made
  venues session-aware (#385), and `place_order` gained `idempotency_key`
  (#419). Both carry defaults so no CALLER breaks, but a third-party adapter
  that does not accept them is no longer a `Broker` -- the conformance suite
  now says so. That is exactly the kind of change a patch bump must not
  hide.
* THE OPERATOR CONSOLE. The TUI became keel's console across #399-#408, and
  `keel update` (#415/#417) makes a deployment self-updating.

Every pinned sibling moves with it. The four production distributions are
required `==` at this exact version (`RELEASING.md`, "Release assets"), so a
bump that missed one would install a mixed set -- the `keel-trader 0.5.7`
against `keel-core 0.5.5` failure `keel versions` exists to catch, and which
`~/keel` actually ran across two releases.

Also in this window, on the Robinhood adapter: the best_bid_ask fixture
corrected against the live venue (#414), a credential guard that catches the
error it only claimed to (#416), pre-flight sizing reported on the preview
(#418), transport backoff (#420), and the fenced one-order probe (#421).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant